using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace GraphMyCode.Controls
{
	public class GraphControl : ItemsControl
	{
		#region Dependancy Properties

		public static readonly DependencyProperty IsDraggingSpaceEnabledProperty =
			DependencyProperty.Register("IsDraggingSpaceEnabled", typeof(bool), typeof(GraphControl),
										new PropertyMetadata(false, IsDraggingSpaceEnabledChanged));

		public static readonly DependencyProperty IsZoomResetProperty =
			DependencyProperty.Register("IsZoomReset", typeof(bool), typeof(GraphControl),
										new PropertyMetadata(true, IsZoomResetChanged));

		public static readonly DependencyProperty ItemContainerStyleProperty =
			DependencyProperty.Register("ItemContainerStyle", typeof(Style), typeof(GraphControl), null);

		public static readonly DependencyProperty SearchPropertyProperty =
			DependencyProperty.Register("SearchProperty", typeof(string), typeof(GraphControl), new PropertyMetadata(null));

		public static readonly DependencyProperty SearchTextProperty =
			DependencyProperty.Register("SearchText", typeof(string), typeof(GraphControl),
										new PropertyMetadata(string.Empty, SearchTextChanged));

		public static readonly DependencyProperty SelectedIndexProperty =
			DependencyProperty.Register("SelectedIndex", typeof(int), typeof(GraphControl),
										new PropertyMetadata(-1, SelectedIndexPropertyChanged));

		public static readonly DependencyProperty SelectedItemProperty =
			DependencyProperty.Register("SelectedItem", typeof(object), typeof(GraphControl),
										new PropertyMetadata(null, SelectedItemPropertyChanged));

		public static readonly DependencyProperty ZoomPercentageProperty =
			DependencyProperty.Register("ZoomPercentage", typeof(double), typeof(GraphControl),
										new PropertyMetadata(1.0, ZoomChanged));

		#endregion

		private const double CentrlizeMargin = 50;
		private const int ItemsScreenCount = 80;
		private readonly Dictionary<object, GraphItem> itemContainers;
		private readonly List<GraphItem> selectedItems;
		private double canvasHeight;
		private double canvasWidth;
		private double centerX;
		private double centerY;

		private Point draggingPoint;
		private Point draggingSpacePoint;
		private double itemsWidthConst;

		internal Border MiniMapBorder;
		private Image miniMapImage;
		private double miniMapScale = .2;
		private ScaleTransform miniMapScaleTransform;

		internal Panel ParentPanel;
		private bool performDrag;
		private bool performDragSpace;
		private bool performSelection;

		private SelectionControl selectionControl;
		private TransformationService transformationService;

		public GraphControl()
		{
			DefaultStyleKey = typeof(GraphControl);
			itemContainers = new Dictionary<object, GraphItem>();
			selectedItems = new List<GraphItem>();
			Loaded += (s, e) => RenderMiniMap();
		}

		#region properties

		public string SearchProperty
		{
			get { return (string)GetValue(SearchPropertyProperty); }
			set { SetValue(SearchPropertyProperty, value); }
		}

		public string SearchText
		{
			get { return (string)GetValue(SearchTextProperty); }
			set { SetValue(SearchTextProperty, value); }
		}

		public bool IsZoomReset
		{
			get { return (bool)GetValue(IsZoomResetProperty); }
			set { SetValue(IsZoomResetProperty, value); }
		}

		public double ZoomPercentage
		{
			get { return (double)GetValue(ZoomPercentageProperty); }
			set { SetValue(ZoomPercentageProperty, value); }
		}

		public bool IsDraggingSpaceEnabled
		{
			get { return (bool)GetValue(IsDraggingSpaceEnabledProperty); }
			set { SetValue(IsDraggingSpaceEnabledProperty, value); }
		}

		public Style ItemContainerStyle
		{
			get { return (Style)GetValue(ItemContainerStyleProperty); }
			set { SetValue(ItemContainerStyleProperty, value); }
		}

		public int SelectedIndex
		{
			get { return (int)GetValue(SelectedIndexProperty); }
			set { SetValue(SelectedIndexProperty, value); }
		}

		public object SelectedItem
		{
			get { return GetValue(SelectedItemProperty); }
			set { SetValue(SelectedItemProperty, value); }
		}

		public IEnumerable<GraphItem> SelectedItems
		{
			get { return selectedItems; }
		}

		#endregion

		private static void IsDraggingSpaceEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			bool isEnabled = Convert.ToBoolean(e.NewValue);
			var thisControl = (Control)d;
			thisControl.Cursor = isEnabled ? Cursors.Hand : Cursors.Arrow;
		}

		private static void ZoomChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			double zoomValue = Convert.ToDouble(e.NewValue);
			if (zoomValue == 0)
				return;

			var thisControl = (GraphControl)d;

			if (thisControl.transformationService == null)
				return;

			if (zoomValue < AnimationService.MinZoomValue || zoomValue > AnimationService.MaxZoomValue)
				return;

			thisControl.transformationService.Zoom(zoomValue);

			thisControl.RenderMiniMap();
		}

		private static void IsZoomResetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			bool isZoomReset = Convert.ToBoolean(e.NewValue);

			var thisControl = (GraphControl)d;

			if (isZoomReset)
				thisControl.ResetZoom();
		}

		private static void SearchTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			string text = Convert.ToString(e.NewValue).ToLower();
			string oldText = Convert.ToString(e.OldValue).ToLower();

			if (string.IsNullOrEmpty(text) && string.IsNullOrEmpty(oldText))
				return;

			var thisControl = (GraphControl)d;

			if (string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(oldText))
				thisControl.ResetZoom();
			else
			{
				object node = thisControl.Items.FirstOrDefault(
					n => ((string)GetPropValue(n, thisControl.SearchProperty) ?? string.Empty).ToLower() == text);

				if (node != null)
					thisControl.CentralizeNodes(new[] { node });
			}
		}

		private void GraphControl_SizeChanged(object sender, SizeChangedEventArgs e)
		{
			foreach (GraphItem item in itemContainers.Values)
			{
				if (!item.IsEdgeItem)
				{
					var nodeDesc = (GraphNodeDescriptor)item.ItemDescriptor;
					nodeDesc.CanvasSize = e.NewSize;
				}

				item.ItemDescriptor.UpdatePosition();
			}
		}

		private void ClearHighlighting()
		{
			foreach (GraphItem item in itemContainers.Values)
			{
				item.IsHighlighted = true;
			}
		}

		private void GraphItemMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
		{
			if (!IsDraggingSpaceEnabled)
			{
				var item = sender as GraphItem;
				if (item != null && item.IsEdgeItem)
					return;

				performDrag = false;
				if (item != null)
					item.ReleaseMouseCapture();
			}
		}

		private void GraphItemMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
		{
			if (!IsDraggingSpaceEnabled)
			{
				var item = sender as GraphItem;
				if (item != null && item.IsEdgeItem)
					return;

				performDrag = true;
				draggingPoint = e.GetPosition(this);
				if (item != null)
					item.CaptureMouse();

				e.Handled = true;
			}
		}

		private void RescaleWorld()
		{
			itemsWidthConst = Items.Count > ItemsScreenCount ? (double)Items.Count / ItemsScreenCount : 1;

			ParentPanel.Width = canvasWidth * itemsWidthConst;
			ParentPanel.Height = canvasHeight * itemsWidthConst;

			MiniMapBorder.Width = canvasWidth;
			MiniMapBorder.Height = canvasHeight;

			centerX = (canvasWidth * itemsWidthConst - canvasWidth) / 2;
			centerY = (canvasHeight * itemsWidthConst - canvasHeight) / 2;

			miniMapScale /= itemsWidthConst;
			miniMapScaleTransform = new ScaleTransform { ScaleX = miniMapScale, ScaleY = miniMapScale };

			transformationService.GraphScale = itemsWidthConst;
			transformationService.Translate(-centerX, -centerY);
		}

		internal void RenderMiniMap()
		{
			//transformationService.DeVirtualize();
			MiniMapBorder.BorderThickness = new Thickness(5 * itemsWidthConst * ZoomPercentage);
			var b = new WriteableBitmap(ParentPanel, miniMapScaleTransform);
			MiniMapBorder.BorderThickness = new Thickness(0);
			miniMapImage.Width = b.PixelWidth;
			miniMapImage.Height = b.PixelHeight;
			miniMapImage.Source = b;
			//transformationService.Virtualize();
		}


		private void MiniMapImageMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
		{
			Point position = e.GetPosition(sender as UIElement);

			Point relativePosition = miniMapScaleTransform.Inverse.Transform(position);

			double k = Math.Sqrt(relativePosition.X * relativePosition.X + relativePosition.Y * relativePosition.Y);
			double k1 = Math.Sqrt(position.X * position.X + position.Y * position.Y);

			double coef = k / k1;

			transformationService.Translate(-coef * position.X + centerX / 2, -coef * position.Y + centerY / 2);

			//animationService.MoveAnimated2(-miniMapDragTransform.X, -miniMapDragTransform.Y, ZoomPercentage, ZoomPercentage);
		}

		internal GraphItem GetItem(object item)
		{
			if (IsItemItsOwnContainerOverride(item))
				return (GraphItem)item;

			if (itemContainers.ContainsKey(item))
				return itemContainers[item];

			return null;
		}

		#region Selection

		internal void Select(GraphItem item)
		{
			selectedItems.Add(item);
		}

		internal void Unselect(GraphItem item)
		{
			selectedItems.Remove(item);
		}

		internal void SelectSingleItem(GraphItem graphItem)
		{
			selectedItems.Add(graphItem);
			SelectedItem = graphItem;
		}

		private static void SelectedItemPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
		{
			var thisControl = (GraphControl)obj;

			var oldItem = e.OldValue as GraphItem;
			if (oldItem != null)
				oldItem.Unselect();

			var newItem = e.NewValue as GraphItem;
			if (newItem != null)
				newItem.Select();

			thisControl.SelectedIndex = thisControl.Items.IndexOf(newItem);
		}

		private static void SelectedIndexPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
		{
			var thisControl = obj as GraphControl;

			if (thisControl == null)
				throw new ArgumentNullException();


			var oldIndex = (int)e.OldValue;
			if (oldIndex >= 0 && oldIndex <= thisControl.Items.Count)
				thisControl.itemContainers[thisControl.Items[oldIndex]].Unselect();

			var newIndex = (int)e.NewValue;
			if (newIndex >= 0 && newIndex <= thisControl.Items.Count)
			{
				GraphItem newItem = thisControl.itemContainers[thisControl.Items[newIndex]];
				newItem.Select();
				thisControl.SelectedItem = thisControl.Items.IndexOf(newItem);
			}
		}

		internal void ClearSelection(bool removeSelected)
		{
			selectionControl.StartPoint = new Point(0, 0);
			selectionControl.EndPoint = new Point(0, 0);

			if (removeSelected)
			{
				foreach (GraphItem item in SelectedItems)
				{
					item.Unselect();
				}

				selectedItems.Clear();
			}
		}

		#endregion

		#region Hack methods

		private static object GetPropValue(object item, string propName)
		{
			//TODO: Replace this method.
			Type t = item.GetType();
			PropertyInfo propInfo = t.GetProperty(propName);

			return propInfo == null ? null : propInfo.GetValue(item, null);
		}

		private static bool IsNodeItem(object item)
		{
			//TODO: Replace this method with Style Binding.
			Type t = item.GetType();
			PropertyInfo propInfo = t.GetProperty("CentralPoint");
			return propInfo != null;
		}

		private GraphItem GetGraphItem(object edgeItem, string propertyName)
		{
			//TODO: Replace this method with Style Binding.			
			Type t = edgeItem.GetType();
			PropertyInfo propInfo = t.GetProperty(propertyName);
			return itemContainers[propInfo.GetValue(edgeItem, null)];
		}

		#endregion

		#region Public Methods

		public void RescaleWorld(double width, double height)
		{
			canvasWidth = width;
			canvasHeight = height;
		}

		public void ResetZoom()
		{
			transformationService.TranslateZoomAnimated(-centerX, -centerY, 1, ZoomPercentage);

			foreach (GraphItem item in itemContainers.Values)
			{
				item.IsHighlighted = true;
				item.IsSelected = false;
			}

			RenderMiniMap();
		}

		public void CentralizeNodes(IEnumerable<object> nodes)
		{
			if (nodes == null || nodes.Count() == 0)
				return;

			List<GraphItem> graphItems =
				nodes.Select(n => IsItemItsOwnContainerOverride(n) ? (GraphItem)n : itemContainers[n]).ToList();

			ClearSelection(true);

			if (graphItems.Count == 1)
			{
				graphItems[0].IsSelected = true;
				graphItems.AddRange(graphItems[0].GetAdjacentNodes());
			}

			Rect union = Rect.Empty;

			foreach (GraphItem item in graphItems)
			{
				union.Union(item.Bounds);
			}

			union.X -= CentrlizeMargin;
			union.Width += (2 * CentrlizeMargin);

			union.Y -= CentrlizeMargin;
			union.Height += (2 * CentrlizeMargin);

			double ratioX = ActualWidth / union.Width;
			double ratioY = ActualHeight / union.Height;

			double translateX = ActualWidth / 2 - (union.X + union.Width / 2);
			double translateY = ActualHeight / 2 - (union.Y + union.Height / 2);

			double zoomValue = ratioX < ratioY ? ratioX : ratioY;

			HighlightNodes(graphItems.Cast<object>());

			transformationService.TranslateZoomAnimated(translateX, translateY, zoomValue > 2 ? 2 : zoomValue, ZoomPercentage);
		}

		public void HighlightNodes(IEnumerable<object> nodes)
		{
			foreach (GraphItem item in itemContainers.Values)
			{
				item.IsHighlighted = false;
			}

			IEnumerable<GraphItem> graphItems =
				nodes.Select(n => IsItemItsOwnContainerOverride(n) ? (GraphItem)n : itemContainers[n]);

			foreach (GraphItem item in graphItems)
			{
				item.IsHighlighted = true;
			}

			IEnumerable<GraphItem> edges = graphItems.SelectMany(x => ((GraphNodeDescriptor)x.ItemDescriptor).OutEdges).
				Intersect(graphItems.SelectMany(x => ((GraphNodeDescriptor)x.ItemDescriptor).InEdges));

			foreach (GraphItem edge in edges)
			{
				edge.IsHighlighted = true;
			}
		}

		public void SelectAdjacent(object item)
		{
			GraphItem graphItem = IsItemItsOwnContainerOverride(item) ? (GraphItem)item : itemContainers[item];
			graphItem.SelectAdjacent();
		}

		public void FilterNodes(IEnumerable<object> nodes)
		{
			foreach (GraphItem item in itemContainers.Values)
			{
				item.IsFiltered = false;
			}

			IEnumerable<GraphItem> graphItems =
				nodes.Select(n => IsItemItsOwnContainerOverride(n) ? (GraphItem)n : itemContainers[n]);

			foreach (GraphItem item in graphItems)
			{
				item.IsFiltered = true;
			}
		}

		#endregion

		#region Override Methods

		protected override bool IsItemItsOwnContainerOverride(object item)
		{
			return item is GraphItem;
		}

		public override void OnApplyTemplate()
		{
			base.OnApplyTemplate();

			selectionControl = GetTemplateChild("PART_SelectionControl") as SelectionControl;
			ParentPanel = GetTemplateChild("PART_ParentPanel") as Panel;
			MiniMapBorder = GetTemplateChild("MiniMapBorder") as Border;
			miniMapImage = GetTemplateChild("MiniMapImage") as Image;

			var zoomTransform = GetTemplateChild("ZoomTransform") as ScaleTransform;
			var dragTransform = GetTemplateChild("DragTransform") as TranslateTransform;
			var miniMapDragTransform = GetTemplateChild("MiniMapDragTransform") as TranslateTransform;
			var miniMapZoomTransform = GetTemplateChild("MiniMapZoomTransform") as ScaleTransform;
			var parentProjection = GetTemplateChild("PanelProjection") as PlaneProjection;

			transformationService = new TransformationService(this,
															  zoomTransform,
															  dragTransform,
															  miniMapDragTransform,
															  miniMapZoomTransform,
															  parentProjection);

			if (miniMapImage != null)
				miniMapImage.MouseLeftButtonDown += MiniMapImageMouseLeftButtonDown;

			if (ParentPanel != null)
			{
				RescaleWorld();
				ParentPanel.SizeChanged += GraphControl_SizeChanged;
			}
		}

		protected override void OnMouseWheel(MouseWheelEventArgs e)
		{
			double z = (double)e.Delta / 100;

			double newZoom = ZoomPercentage + z;
			ZoomPercentage = newZoom > AnimationService.MinZoomValue &&
							 newZoom < AnimationService.MaxZoomValue
								? newZoom
								: ZoomPercentage;

			base.OnMouseWheel(e);
		}

		protected override DependencyObject GetContainerForItemOverride()
		{
			return new GraphItem();
		}

		protected override void ClearContainerForItemOverride(DependencyObject element, object item)
		{
			var graphItem = (GraphItem)element;

			graphItem.ParentGraph = null;
			graphItem.Style = null;
			graphItem.DataTemplate = null;
			graphItem.ItemDescriptor = null;

			graphItem.MouseLeftButtonDown -= GraphItemMouseLeftButtonDown;
			graphItem.MouseLeftButtonUp -= GraphItemMouseLeftButtonUp;

			itemContainers.Remove(item);

			base.ClearContainerForItemOverride(element, item);
		}

		protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
		{
			var graphItem = (GraphItem)element;

			graphItem.ParentGraph = this;
			graphItem.Style = ItemContainerStyle;
			graphItem.DataTemplate = ItemTemplate;
			graphItem.MouseLeftButtonDown += GraphItemMouseLeftButtonDown;
			graphItem.MouseLeftButtonUp += GraphItemMouseLeftButtonUp;

			if (IsNodeItem(item))
			{
				var nodeDesc = new GraphNodeDescriptor
								{
									Item = graphItem,
								};
				nodeDesc.SetBinding(GraphNodeDescriptor.CentralPointProperty,
									new Binding("CentralPoint") { Source = item, Mode = BindingMode.TwoWay });
				nodeDesc.SetBinding(GraphItemDescriptor.ShapeProperty,
									new Binding("ShapeGeometry") { Source = item, Mode = BindingMode.OneTime });

				nodeDesc.CanvasSize = new Size(ActualWidth, ActualHeight);
				graphItem.ItemDescriptor = nodeDesc;
			}
			else
			{
				graphItem.ItemDescriptor = new GraphEdgeDescriptor
											{
												Item = graphItem,
												FromItem = GetGraphItem(item, "FromNode"),
												ToItem = GetGraphItem(item, "ToNode")
											};
				graphItem.ItemDescriptor.SetBinding(GraphEdgeDescriptor.ThickProperty,
													new Binding("Thick") { Source = item, Mode = BindingMode.OneTime });
				graphItem.ItemDescriptor.SetBinding(GraphEdgeDescriptor.ActualThickProperty,
													new Binding("ActualThick") { Source = item, Mode = BindingMode.OneTime });
				graphItem.ItemDescriptor.SetBinding(GraphEdgeDescriptor.IsDirectionalProperty,
													new Binding("IsDirectional") { Source = item, Mode = BindingMode.OneTime });
			}

			itemContainers.Add(item, graphItem);

			//base.PrepareContainerForItemOverride(element, item);
		}

		protected override void OnMouseMove(MouseEventArgs e)
		{
			if (performDragSpace)
			{
				Point p = e.GetPosition(this);

				transformationService.MoveTranslate((p.X - draggingSpacePoint.X) / ZoomPercentage,
													(p.Y - draggingSpacePoint.Y) / ZoomPercentage);

				draggingSpacePoint = p;
				IsZoomReset = false;
			}
			else
			{
				if (performDrag)
				{
					Point p = e.GetPosition(this);
					double offsetX = (p.X - draggingPoint.X) / ZoomPercentage;
					double offsetY = (p.Y - draggingPoint.Y) / ZoomPercentage;

					foreach (GraphItem item in selectedItems)
					{
						var nodeDesc = (GraphNodeDescriptor)item.ItemDescriptor;

						nodeDesc.Move(offsetX, offsetY);
					}

					draggingPoint = p;
				}
				if (performSelection)
				{
					selectionControl.EndPoint = e.GetPosition(ParentPanel);
					//selectionControl.RenderTransform = dragTransform;

					foreach (GraphItem node in itemContainers.Values.Where(i => !i.IsEdgeItem))
					{
						node.IsSelected = node.Bounds.IntersectsWith(selectionControl.Bounds);
					}
				}
			}
			base.OnMouseMove(e);
		}

		protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
		{
			if (performDragSpace)
			{
				performDragSpace = false;
			}
			else
			{
				performSelection = false;
				ClearSelection(false);
				selectionControl.ReleaseMouseCapture();
			}
			RenderMiniMap();

			base.OnMouseLeftButtonUp(e);
		}

		protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
		{
			if (IsDraggingSpaceEnabled)
			{
				draggingSpacePoint = e.GetPosition(this);
				performDragSpace = true;
			}
			else
			{
				ClearSelection(true);

				ClearHighlighting();

				performSelection = true;
				selectionControl.StartPoint = e.GetPosition(ParentPanel);
				selectionControl.CaptureMouse();
			}
			base.OnMouseLeftButtonDown(e);
		}

		#endregion
	}
}